package gsw.tool.router.compiler.processor;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.WildcardTypeName;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

import gsw.tool.router.annotation.Route;
import gsw.tool.router.annotation.model.RouteMeta;
import gsw.tool.router.compiler.utils.Consts;
import gsw.tool.router.compiler.utils.Log;
import gsw.tool.router.compiler.utils.Utils;

import static javax.lang.model.element.Modifier.PUBLIC;

//Automatically compile the Processor class.
@AutoService(Processor.class)
//The processor receives the parameter instead of the {@link AbstractProcessor#getSupportedOptions()} function.
@SupportedOptions(Consts.ARGUMENTS_NAME)
//Specified using Java version replace {@ link AbstractProcessor# getSupportedSourceVersion} () function
@SupportedSourceVersion(SourceVersion.RELEASE_7)
//Register which annotations to replace {@ link AbstractProcessor# getSupportedAnnotationTypes} () function
@SupportedAnnotationTypes({Consts.ANN_TYPE_ROUTE})
public class RouteProcessor extends AbstractProcessor {
    //Key: group name value: class name.
    private Map<String, String> rootMap = new TreeMap<>();
    //Group key: group name value: routing information for the corresponding group.
    private Map<String, List<RouteMeta>> groupMap = new HashMap<>();
    //Node utility classes (classes, functions, properties are nodes)
    private Elements elementUtils;
    //Type (class information) utility class.
    private Types typeUtils;
    //File generator class/resource.
    private Filer filerUtils;
    //parameter
    private String moduleName;
    //Custom logging tools.
    private Log log;

    /**
     * Initializes a series of processor tools from the {@link ProcessingEnvironment}.
     *
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //Get apt log output.
        log = Log.newLog(processingEnvironment.getMessager());
        //Access to the node
        elementUtils = processingEnvironment.getElementUtils();
        //Access to type
        typeUtils = processingEnvironment.getTypeUtils();
        //Get the file generator.
        filerUtils = processingEnvironment.getFiler();
        //The argument is that the module name generates the same xx$ROOT$$file to prevent multi-module/componentized development.
        Map<String, String> options = processingEnvironment.getOptions();
        if (!Utils.isEmpty(options)) {
            moduleName = options.get(Consts.ARGUMENTS_NAME);
        }
        log.i("RouteProcessor Parmaters:" + moduleName);
        if (Utils.isEmpty(moduleName)) {
            throw new RuntimeException("Not set Processor Parmaters.");
        }
    }

    /**
     * The equivalent of the main function, formally processing the annotations.
     *
     * @param set              A collection of nodes that support processing annotations is used.
     * @param roundEnvironment Represents the current or previous operating environment, which can be found through the object lookup.
     * @return True means that the subsequent processor will not be processed (processed)
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //If there is an annotation that needs to be processed, it is processed.
        if (!Utils.isEmpty(set)) {
            //Gets the collection of all elements that are annotated by the Route.
            Set<? extends Element> routeElements = roundEnvironment.getElementsAnnotatedWith(Route.class);
            if (!Utils.isEmpty(routeElements)) {
                try {
                    //Processing Route annotations
                    parseRoutes(routeElements);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return true;
        }
        return false;
    }

    /**
     * Processing Route annotations
     *
     * @param routeElements All set of elements that are annotated by the Route.
     */
    private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
        //Support for configuring the types of routing classes.
        TypeElement activity = elementUtils.getTypeElement(Consts.ACTIVITY);
        //The node describes the Mirror.
        TypeMirror type_Activity = activity.asType();

        TypeElement iService = elementUtils.getTypeElement(Consts.ISERVICE);
        TypeMirror type_IService = iService.asType();

        /**
         * GroupMap (group name: routing information) collection.
         */
        //The node that declares the Route annotation (the node Activity/IService that needs to be processed)
        for (Element element : routeElements) {
            //Routing information
            RouteMeta routeMeta;
            //Use the Route annotated class information.
            TypeMirror tm = element.asType();
            log.i("Route Class: " + tm.toString());
            Route route = element.getAnnotation(Route.class);
            //Whether the Activity USES the Route annotation.
            if (typeUtils.isSubtype(tm, type_Activity)) {
                routeMeta = new RouteMeta(RouteMeta.Type.ACTIVITY, route, element);
            } else if (typeUtils.isSubtype(tm, type_IService)) {
                routeMeta = new RouteMeta(RouteMeta.Type.ISERVICE, route, element);
            } else {
                throw new RuntimeException("[Just Support Activity/IService Route] :" + element);
            }
            //Group map <Group Group,RouteMeta routing information > collection.
            categories(routeMeta);
        }

        //Generate the interfaces that the class needs to implement.
        TypeElement iRouteGroup = elementUtils.getTypeElement(Consts.IROUTE_GROUP);
        TypeElement iRouteRoot = elementUtils.getTypeElement(Consts.IROUTE_ROOT);

        //Generate Group class
        generatedGroup(iRouteGroup);
        //Generate the Root class
        generatedRoot(iRouteRoot, iRouteGroup);
    }

    /**
     * Generate Group Class action: record < address,RouteMeta routing information (Class file, etc.)>.
     */
    private void generatedGroup(TypeElement iRouteGroup) throws IOException {
        //Parameter Map < String, RouteMeta >
        ParameterizedTypeName atlas = ParameterizedTypeName.get(
                ClassName.get(Map.class),
                ClassName.get(String.class),
                ClassName.get(RouteMeta.class)
        );
        //Parameter Map < String, RouteMeta > atlas
        ParameterSpec groupParamSpec = ParameterSpec.builder(atlas, "atlas")
                .build();

        //Traversal groups, each Group creates a $$Group goeast class.
        for (Map.Entry<String, List<RouteMeta>> entry : groupMap.entrySet()) {
            /**
             * Class member function loadInfo declares the build.
             */
            //function:public void loadInfo(Map<String,RouteMeta> atlas)
            MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder
                    (Consts.METHOD_LOAD_INTO)
                    .addAnnotation(Override.class)
                    .addModifiers(PUBLIC)
                    .addParameter(groupParamSpec);

            //Grouping names and information in the corresponding groups.
            String groupName = entry.getKey();
            List<RouteMeta> groupData = entry.getValue();
            //Iterate through the entry data in the group.
            for (RouteMeta routeMeta : groupData) {
                //Assembly function :atlas. Put (address, routemet.build (Class,path,group))
                loadIntoMethodOfGroupBuilder.addStatement(
                        /**
                         * $S represent String,  refer to https://github.com/square/javapoet#s-for-strings
                         * $T represent Type, refer to https://github.com/square/javapoet#t-for-types
                         * $L represent constants, refer to https://github.com/square/javapoet#l-for-literals
                         */
                        "atlas.put($S, $T.build($T.$L,$T.class, $S, $S))",
                        routeMeta.getPath(),
                        ClassName.get(RouteMeta.class),
                        ClassName.get(RouteMeta.Type.class),
                        routeMeta.getType(),
                        ClassName.get((TypeElement) routeMeta.getElement()),
                        routeMeta.getPath().toLowerCase(),
                        routeMeta.getGroup().toLowerCase());
            }
            //Create a Java file ($$Group$) Group.
            String groupClassName = Consts.NAME_OF_GROUP + groupName;
            JavaFile.builder(Consts.PACKAGE_OF_GENERATE_FILE,
                    TypeSpec.classBuilder(groupClassName)
                            .addSuperinterface(ClassName.get(iRouteGroup))
                            .addModifiers(PUBLIC)
                            .addMethod(loadIntoMethodOfGroupBuilder.build())
                            .build()
            ).build().writeTo(filerUtils);
            log.i("Generated RouteGroup: " + Consts.PACKAGE_OF_GENERATE_FILE + "." +
                    groupClassName);
            //Group names and generated corresponding Group class names.
            rootMap.put(groupName, groupClassName);
        }
    }

    /**
     * Generate Root class action: record < Group, corresponding Group class >.
     */
    private void generatedRoot(TypeElement iRouteRoot, TypeElement iRouteGroup) throws IOException {
        //Type:Map<String,Class<? extends IRouteGroup>> routes>
        ParameterizedTypeName routes = ParameterizedTypeName.get(
                ClassName.get(Map.class),
                ClassName.get(String.class),
                ParameterizedTypeName.get(
                        ClassName.get(Class.class),
                        WildcardTypeName.subtypeOf(ClassName.get(iRouteGroup))
                )
        );

        //parameter Map<String,Class<? extends IRouteGroup>> routes> routes
        ParameterSpec rootParamSpec = ParameterSpec.builder(routes, "routes")
                .build();
        //function public void loadInfo(Map<String,Class<? extends IRouteGroup>> routes> routes)
        MethodSpec.Builder loadIntoMethodOfRootBuilder = MethodSpec.methodBuilder
                (Consts.METHOD_LOAD_INTO)
                .addAnnotation(Override.class)
                .addModifiers(PUBLIC)
                .addParameter(rootParamSpec);

        //function
        for (Map.Entry<String, String> entry : rootMap.entrySet()) {
            loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry
                    .getKey(), ClassName.get(Consts.PACKAGE_OF_GENERATE_FILE, entry.getValue
                    ()));
        }
        //Generate $Root$ class
        String rootClassName = Consts.NAME_OF_ROOT + moduleName;
        JavaFile.builder(Consts.PACKAGE_OF_GENERATE_FILE,
                TypeSpec.classBuilder(rootClassName)
                        .addSuperinterface(ClassName.get(iRouteRoot))
                        .addModifiers(PUBLIC)
                        .addMethod(loadIntoMethodOfRootBuilder.build())
                        .build()
        ).build().writeTo(filerUtils);

        log.i("Generated RouteRoot: " + Consts.PACKAGE_OF_GENERATE_FILE + "." + rootClassName);
    }

    /**
     * Group map <Group Group,RouteMeta routing information > collection.
     *
     * @param routeMeta
     */
    private void categories(RouteMeta routeMeta) {
        if (routeVerify(routeMeta)) {
            log.i("Group Info, Group Name = " + routeMeta.getGroup() + ", Path = " +
                    routeMeta.getPath());
            List<RouteMeta> routeMetas = groupMap.get(routeMeta.getGroup());
            //If an unrecorded group is created.
            if (Utils.isEmpty(routeMetas)) {
                List<RouteMeta> routeMetaSet = new ArrayList<>();
                routeMetaSet.add(routeMeta);
                groupMap.put(routeMeta.getGroup(), routeMetaSet);
            } else {
                routeMetas.add(routeMeta);
            }
        } else {
            log.i("Group Info Error: " + routeMeta.getPath());
        }
    }

    /**
     * Verify that the routing information must have a path(and set the grouping)
     *
     * @param meta raw meta
     */
    private boolean routeVerify(RouteMeta meta) {
        String path = meta.getPath();
        String group = meta.getGroup();
        //Routing addresses must be start by "/".
        if (Utils.isEmpty(path) || !path.startsWith("/")) {
            return false;
        }
        //If no grouping is set, the first/second nodes are grouped (so you must path two /)
        if (Utils.isEmpty(group)) {
            String defaultGroup = path.substring(1, path.indexOf("/", 1));
            if (Utils.isEmpty(defaultGroup)) {
                return false;
            }
            meta.setGroup(defaultGroup);
            return true;
        }
        return true;
    }

}
